﻿using System;
using System.Diagnostics;

namespace DotNetRepository.Tools
{
    /// PreciseClock provides a way to get a DateTime that exhibits the
    /// relative precision of System.Diagnostics.Stopwatch, and the absolute 
    /// accuracy of DateTime.Now.
    public class PreciseClock
    {
        private const long ClockTickFrequency = 10000000;

        /// The internal System.Diagnostics.Stopwatch used by this instance.
        private readonly Stopwatch _stopwatch;

        private readonly decimal _clockTicksPerStopwatchTick;
        private readonly long _startingClockTicks;
        private readonly long _synchronizePeriodTicks;
        private PreciseClockSafeImmutable _startingClock;
        private readonly object _lockObj = new object();

        /// Creates a new instance of PreciseClock.
        /// A large value of synchronizePeriodSeconds may cause arithmetic overthrow
        /// exceptions to be thrown. A small value may cause the time to be unstable.
        /// A good value is 10.
        /// synchronizePeriodSeconds = The number of seconds after which the
        /// PreciseClock will synchronize itself with the system clock.
        public PreciseClock(long synchronizePeriodSeconds)
        {
            _startingClockTicks = DateTime.UtcNow.Ticks;
            _stopwatch = Stopwatch.StartNew();

            _clockTicksPerStopwatchTick = (decimal)ClockTickFrequency / Stopwatch.Frequency;
            _startingClock = new PreciseClockSafeImmutable(_startingClockTicks, 0);

            _synchronizePeriodTicks = synchronizePeriodSeconds * ClockTickFrequency;
        }

        /// Returns the current date and time, just like DateTime.UtcNow.
        public DateTime UtcNow
        {
            get
            {
                lock (_lockObj)
                {
                    var currentTimerTicks = ConvertToClockTicks(_stopwatch.ElapsedTicks);
                    var currentTime = DateTime.UtcNow;
                    var utcNow = new DateTime(_startingClock.ClockTicks + currentTimerTicks - _startingClock.TimerTicks);

                    // If SyncronizationPeriod has expired then synchronize
                    if (currentTimerTicks >= _startingClock.TimerTicks + _synchronizePeriodTicks)
                    {
                        var timeTicksElapsed = currentTime.Ticks - _startingClockTicks;

                        // If the difference between the calculated time elapsed and the stopwatch time elapsed 
                        // is greater than 10ms then we need to synchronize
                        if (Math.Abs(currentTimerTicks - timeTicksElapsed) > 160000)
                        {
                            utcNow = utcNow.AddTicks(timeTicksElapsed - currentTimerTicks);
                            _startingClock = new PreciseClockSafeImmutable(utcNow.Ticks, currentTimerTicks);
                        }
                    }
                    return utcNow;
                }
            }
        }

        /// Returns the current date and time, just like DateTime.Now.
        public DateTime Now
        {
            get { return UtcNow.ToLocalTime(); }
        }

        private long ConvertToClockTicks(long stopwatchTicks)
        {
            return (stopwatchTicks * Decimal.ToInt64(_clockTicksPerStopwatchTick * 10000)) / 10000;
        }

        #region Nested type: PreciseClockSafeImmutable

        private sealed class PreciseClockSafeImmutable
        {
            internal readonly long ClockTicks;
            internal readonly long TimerTicks;

            internal PreciseClockSafeImmutable(long clockTicks, long timerTicks)
            {
                ClockTicks = clockTicks;
                TimerTicks = timerTicks;
            }
        }

        #endregion
    }
}